using Cysharp.Threading.Tasks;
using EarthGeospatial;
using Geometry;
using System.Collections.Generic;
using System;
using System.Xml;
using UnityEngine;
using Unity.Mathematics;

namespace Cesium3DTilesSelection
{
    public struct TileMapServiceRasterOverlayOptions
    {
        /**
         * @brief The file extension for images on the server.
         */
        public string fileExtension;

        /**
         * @brief A credit for the data source, which is displayed on the canvas.
         */
        public string credit;

        /**
         * @brief The minimum level-of-detail supported by the imagery provider.
         *
         * Take care when specifying this that the number of tiles at the minimum
         * level is small, such as four or less. A larger number is likely to
         * result in rendering problems.
         */
        public int? minimumLevel;

        /**
         * @brief The maximum level-of-detail supported by the imagery provider.
         *
         * This will be `std::nullopt` if there is no limit.
         */
        public int? maximumLevel;

        /**
         * @brief The {@link Rectangle}, in radians, covered by the
         * image.
         */
        public Rectangle coverageRectangle;

        /**
         * @brief The {@link CesiumGeospatial::Projection} that is used.
         */
        public Projection projection;

        /**
         * @brief The {@link QuadtreeTilingScheme} specifying how
         * the ellipsoidal surface is broken into tiles.
         */
        public QuadtreeTilingScheme tilingScheme;

        /**
         * @brief The {@link CesiumGeospatial::Ellipsoid}.
         *
         * If the `tilingScheme` is specified, this parameter is ignored and
         * the tiling scheme's ellipsoid is used instead. If neither parameter
         * is specified, the {@link CesiumGeospatial::Ellipsoid::WGS84} is used.
         */
        public Ellipsoid ellipsoid;

        /**
         * @brief Pixel width of image tiles.
         */
        public int tileWidth;

        /**
         * @brief Pixel height of image tiles.
         */
        public int tileHeight;

        /**
         * @brief An otion to flip the x- and y values of a tile map resource.
         *
         * Older versions of gdal2tiles.py flipped X and Y values in
         * `tilemapresource.xml`. Specifying this option will do the same, allowing
         * for loading of these incorrect tilesets.
         */
        public bool flipXY;

    }

    public class TileMapServiceTileProvider : QuadtreeRasterOverlayTileProvider
    {
        private string _url;
        private Dictionary<string, string> _headers;
        private string _fileExtension;

        public TileMapServiceTileProvider(RasterOverlay owner, IPrepareRendererResources pPrepareRendererResources, Projection projection,
         QuadtreeTilingScheme tilingScheme, Rectangle coverageRectangle,
         string url, Dictionary<string, string> headers,
         string fileExtension,
        int width,
        int height,
        int minimumLevel,
        int maximumLevel) :
          base(owner, pPrepareRendererResources, projection, tilingScheme, coverageRectangle, minimumLevel, maximumLevel, width, height)
        {
            this._url = url;
            this._headers = headers;
            this._fileExtension = fileExtension;
        }

        protected override async UniTask<LoadedRasterOverlayImage> loadQuadtreeTileImage(QuadtreeTileID tileID)
        {
            string url = this._url  + tileID.level + "/" + tileID.x + "/" + tileID.y + this._fileExtension;
            LoadTileImageFromUrlOptions options = new LoadTileImageFromUrlOptions();
            options.rectangle = this.tilingScheme.tileToRectangle(tileID);
            options.moreDetailAvailable = tileID.level < this.MaximumLevel;
            return await this.loadTileImageFromUrl(url, this._headers, options);
        }
    }



    public class TileMapServiceRasterOverlay : RasterOverlay
    {

        private string url;
        private Dictionary<string, string> headers;
        private TileMapServiceRasterOverlayOptions options;
        public TileMapServiceRasterOverlay(string name, string url, Dictionary<string, string> header, TileMapServiceRasterOverlayOptions options=new TileMapServiceRasterOverlayOptions())
        {
            this.name=name;
            this.url=url;
            this.headers=header;
            this.options = options;
        }

        public override async UniTask<RasterOverlayTileProvider> createTileProvider(IPrepareRendererResources pPrepareRendererResources, RasterOverlay pOwner)
        {

            Uri xmlUrl = new Uri(new Uri(this.url), "tilemapresource.xml");
            pOwner = pOwner ?? this;
            var xmldata = await http.Get(xmlUrl.AbsoluteUri,null,this.headers);

            XmlDocument doc = new XmlDocument();
            doc.LoadXml(xmldata);
            var pRoot = doc.DocumentElement;
            if (pRoot == null)
            {
                Debug.Log("ile map service XML document does not have a root element.");
            }
            var pTileFormat = (XmlElement)pRoot.SelectSingleNode("TileFormat");
            string fileExtension = pTileFormat.GetAttribute("extension") ?? "png";

            bool haveW = int.TryParse(pTileFormat.GetAttribute("width"), out int tileWidth);
            bool haveH = int.TryParse(pTileFormat.GetAttribute("height"), out int tileHeight);


            tileWidth = this.options.tileWidth > 0 ? this.options.tileWidth : haveW ? tileWidth : 256;
            tileHeight = this.options.tileHeight > 0 ? this.options.tileHeight : haveH ? tileHeight : 256;

            int minimumLevel = int.MaxValue;
            int maximumLevel = 0;

            var pTilesets = pRoot.SelectSingleNode("TileSets");
            if (pTilesets != null)
            {
                var tileSetList = pTilesets.SelectNodes("TileSet");
                foreach (XmlNode item in tileSetList)
                {
                    var pTileset = (XmlElement)item;
                    var order = pTileset.GetAttribute("order");
                    int level = int.TryParse(order, out int l) ? l : 0;
                    minimumLevel = math.min(minimumLevel, level);
                    maximumLevel = math.max(maximumLevel, level);
                }
            }
            if (maximumLevel < minimumLevel && maximumLevel == 0)
            {
                // Min and max levels unknown, so use defaults.
                minimumLevel = 0;
                maximumLevel = 25;
            }

            GlobeRectangle tilingSchemeRectangle = GeographicProjection.MAXIMUM_GLOBE_RECTANGLE;
            Projection projection = null;
            int rootTilesX = 1;
            bool isRectangleInDegrees = false;

            if (options.projection != null)
            {
                projection = options.projection;
            }
            else
            {
                var pTilesetsElement = (XmlElement)pTilesets;
                string projectionName = pTilesetsElement.GetAttribute("profile") ?? "mercator";

                if (projectionName == "mercator" ||
                    projectionName == "global-mercator")
                {
                    projection = new Projection();
                    projection.useProjection(new WebMercatorProjection());
                    tilingSchemeRectangle = WebMercatorProjection.MAXIMUM_GLOBE_RECTANGLE;

                    // Determine based on the profile attribute if this tileset was
                    // generated by gdal2tiles.py, which uses 'mercator' and
                    // 'geodetic' profiles, or by a tool compliant with the TMS
                    // standard, which is 'global-mercator' and 'global-geodetic'
                    // profiles. In the gdal2Tiles case, X and Y are always in
                    // geodetic degrees.
                    isRectangleInDegrees = projectionName.Contains("global-");
                }
                else if (
                  projectionName == "geodetic" ||
                  projectionName == "global-geodetic")
                {
                    projection = new Projection();
                    projection.useProjection(new GeographicProjection());
                    tilingSchemeRectangle = GeographicProjection.MAXIMUM_GLOBE_RECTANGLE;
                    rootTilesX = 2;

                    // The geodetic profile is always in degrees.
                    isRectangleInDegrees = true;
                }
            }

            minimumLevel = math.min(minimumLevel, maximumLevel);

            minimumLevel = options.minimumLevel ?? minimumLevel;
            maximumLevel = options.maximumLevel ?? maximumLevel;

            Rectangle coverageRectangle = Projection.projectRectangleSimple(projection, tilingSchemeRectangle);

            if (options.coverageRectangle)
            {
                coverageRectangle = options.coverageRectangle;
            }
            else
            {
                var pBoundingBox = pRoot.SelectSingleNode("BoundingBox");

                double? west = getAttributeDouble(pBoundingBox, "minx");
                double? south = getAttributeDouble(pBoundingBox, "miny");
                double? east = getAttributeDouble(pBoundingBox, "maxx");
                double? north = getAttributeDouble(pBoundingBox, "maxy");

                if (west.HasValue && south.HasValue && east.HasValue && north.HasValue)
                {
                    if (isRectangleInDegrees)
                    {
                        coverageRectangle = Projection.projectRectangleSimple(projection, GlobeRectangle.fromDegrees(
                                west.Value,
                                south.Value,
                                east.Value,
                                north.Value
                                ));
                    }
                    else
                    {
                        coverageRectangle = new Rectangle(
                                west.Value,
                                south.Value,
                                east.Value,
                                north.Value
                            );
                    }
                }
            }

            QuadtreeTilingScheme tilingScheme = new QuadtreeTilingScheme(
                Projection.projectRectangleSimple(projection, tilingSchemeRectangle),
                rootTilesX,
                1);

            return new TileMapServiceTileProvider(pOwner, pPrepareRendererResources,
                projection,
                tilingScheme,
                coverageRectangle,
                url,
                headers,
                !string.IsNullOrEmpty(fileExtension) ? "." + fileExtension : fileExtension,
                tileWidth,
                tileHeight,
                minimumLevel,
                maximumLevel);
        }
        private double? getAttributeDouble(XmlNode node, string v)
        {
            var element = (XmlElement)node;
            var value = element.GetAttribute(v);
            if (double.TryParse(value, out double d)) return d;
            return null;
        }
    }
}

